package Zim::Components::PageView;

use strict;
use vars '$AUTOLOAD';
use POSIX qw(strftime);
use Gtk2;
use Gtk2::Pango;               # pango constants
use Gtk2::Ex::HyperTextView;   # custom widget
use Gtk2::Ex::HyperTextBuffer; #    "     "
use Gtk2::Gdk::Keysyms;
use Gtk2::Ex::DesktopEntryMenu;    # for file:// links

our $VERSION = '0.15';

# TODO move more logic into Gtk2::Ex::HyperTextBuffer !

=head1 NAME

Zim::Components::PageView - Page TextView widgets

=head1 DESCRIPTION

This module contains the widgets to display an editable
text buffer containing the current page. It includes a search entry
at the bottom of the TextView, formatting codes for the TextBuffer and 
an undo stack.

=head1 METHODS

Undefined methods are AUTOLOADED to the Gtk2::Ex::HyperTextView object.

=over 4

=cut

our $LINK_ICON = Gtk2->CHECK_VERSION(2, 6, 0) ? 'gtk-connect' : 'gtk-convert';
	# gtk-connect stock item was introduced in 2.6.0

my @ui_actions = (
	# name,          stock id,           label,            accelerator,       tooltip
	[ 'Head1',       undef,              'Head _1',        '<ctrl>1',         'Heading 1',     \&on_tag         ],
	[ 'Head2',       undef,              'Head _2',        '<ctrl>2',         'Heading 2',     \&on_tag         ],
	[ 'Head3',       undef,              'Head _3',        '<ctrl>3',         'Heading 3',     \&on_tag         ],
	[ 'Head4',       undef,              'Head _4',        '<ctrl>4',         'Heading 4',     \&on_tag         ],
	[ 'Head5',       undef,              'Head _5',        '<ctrl>5',         'Heading 5',     \&on_tag         ],
	[ 'Normal',      undef,              '_Normal',        '<ctrl>6',         'Normal',        \&on_tag         ],
	[ 'Bold',        'gtk-bold',         '_Bold',          '<ctrl>B',         'Bold',          \&on_tag         ],
	[ 'Italic',      'gtk-italic',       '_Italic',        '<ctrl>I',         'Italic',        \&on_tag         ],
	[ 'Underline',   'gtk-underline',    '_Underline',     '<ctrl>U',         'Underline',     \&on_tag         ],
	[ 'Strike',      'gtk-strikethrough','Stri_ke',        '<ctrl>K',         'Strike',        \&on_tag         ],
	[ 'Verbatim',    undef,              '_Verbatim',      '<ctrl>T',         'Verbatim',      \&on_tag         ],
	[ 'SplitPage',   undef,              'Spli_t page',    undef,             'Split page',    \&on_SplitPage   ],
	[ 'MergePage',   undef,              '_Merge page',    undef,             'Merge page',    \&on_MergePage   ],
	[ 'Undo',        'gtk-undo',         '_Undo',          '<ctrl>Z',         'Undo',          \&on_Undo        ],
	[ 'Redo',        'gtk-redo',         '_Redo',          '<ctrl><shift>Z',  'Redo',          \&on_Redo        ],
	[ 'Cut',         'gtk-cut',          'Cu_t',           '<ctrl>X',         'Cut',           \&on_clipboard   ],
	[ 'Copy',        'gtk-copy',         '_Copy',          '<ctrl>C',         'Copy',          \&on_clipboard   ],
	[ 'Paste',       'gtk-paste',        '_Paste',         '<ctrl>V',         'Paste',         \&on_clipboard   ],
	[ 'Delete',      'gtk-delete',       'Delete',         'Delete',          'Delete',        \&on_Delete      ],
	[ 'EditLink',    'gtk-properties',   '_Link',          '<ctrl>E',         'Edit link',     \&on_EditLink    ],
	[ 'InsertDate',  undef,              '_Date and Time', '<ctrl>D',         'Insert date',   \&on_InsertDate  ],
	[ 'InsertLink',  $LINK_ICON,         '_Link...',       undef,             'Insert link',   \&on_EditLink    ],
	[ 'InsertImage', undef,              '_Image...',      undef,             'Insert image',  \&on_InsertImage ],
);

my @ui_actions_ro = (
	[ 'Link',        $LINK_ICON,       '_Link',          '<ctrl>L',         'Link',          \&on_Link        ],
	[ 'Find',        'gtk-find',       '_Find',          '<ctrl>F',         'Find',          \&on_Find        ],
	[ 'FindNext',    undef,            'Find Ne_xt',     '<ctrl>G',         'Find next',     \&on_FindNext    ],
	[ 'FindPrev',    undef,            'Find Pre_vious', '<ctrl><shift>G',  'Find previous', \&on_FindPrev    ],
);

#		<menu action='FileMenu'>
#			<placeholder name='FileMods'>
#				<menuitem action='SplitPage'/>
#				<menuitem action='MergePage'/>
#			</placeholder>
#		</menu>

my $ui_layout = q{<ui>
	<menubar name='MenuBar'>
		<menu action='EditMenu'>
			<placeholder name='EditPage'>
				<menuitem action='Undo'/>
				<menuitem action='Redo'/>
				<separator/>
				<menuitem action='Cut'/>
				<menuitem action='Copy'/>
				<menuitem action='Paste'/>
				<menuitem action='Delete'/>
				<separator/>
				<menuitem action='EditLink'/>
				<separator/>
			</placeholder>
		</menu>
		<menu action='InsertMenu'>
			<menuitem action='InsertDate'/>
			<separator/>
			<menuitem action='InsertImage'/>
			<menuitem action='InsertLink'/>
		</menu>
		<menu action='FormatMenu'>
			<menuitem action='Head1'/>
			<menuitem action='Head2'/>
			<menuitem action='Head3'/>
			<menuitem action='Head4'/>
			<menuitem action='Head5'/>
			<menuitem action='Normal'/>
			<separator/>
			<menuitem action='Bold'/>
			<menuitem action='Italic'/>
			<menuitem action='Underline'/>
			<menuitem action='Strike'/>
			<menuitem action='Verbatim'/>
			<separator/>
			<menuitem action='Link'/>
		</menu>
	</menubar>
	<toolbar name='ToolBar'>
		<placeholder name='Format'>
			<toolitem action='Link'/>
			<toolitem action='EditLink'/>
			<separator/>
			<toolitem action='Bold'/>
			<toolitem action='Italic'/>
			<toolitem action='Underline'/>
		</placeholder>
		<separator/>
	</toolbar>
</ui>};

my $ui_layout_ro = q{<ui>
	<menubar name='MenuBar'>
		<menu action='EditMenu'>
			<placeholder name='EditPage'>
				<menuitem action='Find'/>
				<menuitem action='FindNext'/>
				<menuitem action='FindPrev'/>
			</placeholder>
		</menu>
	</menubar>
	<accelerator action='Link'/>
</ui>};

## Table below copied from HTML::Entities by Gisle Aas ##
our %entity2char = (
 # Some normal chars that have special meaning in SGML context
 amp    => '&',  # ampersand 
'gt'    => '>',  # greater than
'lt'    => '<',  # less than
 quot   => '"',  # double quote
 apos   => "'",  # single quote

 # PUBLIC ISO 8879-1986//ENTITIES Added Latin 1//EN//HTML
 AElig	=> chr(198),  # capital AE diphthong (ligature)
 Aacute	=> chr(193),  # capital A, acute accent
 Acirc	=> chr(194),  # capital A, circumflex accent
 Agrave	=> chr(192),  # capital A, grave accent
 Aring	=> chr(197),  # capital A, ring
 Atilde	=> chr(195),  # capital A, tilde
 Auml	=> chr(196),  # capital A, dieresis or umlaut mark
 Ccedil	=> chr(199),  # capital C, cedilla
 ETH	=> chr(208),  # capital Eth, Icelandic
 Eacute	=> chr(201),  # capital E, acute accent
 Ecirc	=> chr(202),  # capital E, circumflex accent
 Egrave	=> chr(200),  # capital E, grave accent
 Euml	=> chr(203),  # capital E, dieresis or umlaut mark
 Iacute	=> chr(205),  # capital I, acute accent
 Icirc	=> chr(206),  # capital I, circumflex accent
 Igrave	=> chr(204),  # capital I, grave accent
 Iuml	=> chr(207),  # capital I, dieresis or umlaut mark
 Ntilde	=> chr(209),  # capital N, tilde
 Oacute	=> chr(211),  # capital O, acute accent
 Ocirc	=> chr(212),  # capital O, circumflex accent
 Ograve	=> chr(210),  # capital O, grave accent
 Oslash	=> chr(216),  # capital O, slash
 Otilde	=> chr(213),  # capital O, tilde
 Ouml	=> chr(214),  # capital O, dieresis or umlaut mark
 THORN	=> chr(222),  # capital THORN, Icelandic
 Uacute	=> chr(218),  # capital U, acute accent
 Ucirc	=> chr(219),  # capital U, circumflex accent
 Ugrave	=> chr(217),  # capital U, grave accent
 Uuml	=> chr(220),  # capital U, dieresis or umlaut mark
 Yacute	=> chr(221),  # capital Y, acute accent
 aacute	=> chr(225),  # small a, acute accent
 acirc	=> chr(226),  # small a, circumflex accent
 aelig	=> chr(230),  # small ae diphthong (ligature)
 agrave	=> chr(224),  # small a, grave accent
 aring	=> chr(229),  # small a, ring
 atilde	=> chr(227),  # small a, tilde
 auml	=> chr(228),  # small a, dieresis or umlaut mark
 ccedil	=> chr(231),  # small c, cedilla
 eacute	=> chr(233),  # small e, acute accent
 ecirc	=> chr(234),  # small e, circumflex accent
 egrave	=> chr(232),  # small e, grave accent
 eth	=> chr(240),  # small eth, Icelandic
 euml	=> chr(235),  # small e, dieresis or umlaut mark
 iacute	=> chr(237),  # small i, acute accent
 icirc	=> chr(238),  # small i, circumflex accent
 igrave	=> chr(236),  # small i, grave accent
 iuml	=> chr(239),  # small i, dieresis or umlaut mark
 ntilde	=> chr(241),  # small n, tilde
 oacute	=> chr(243),  # small o, acute accent
 ocirc	=> chr(244),  # small o, circumflex accent
 ograve	=> chr(242),  # small o, grave accent
 oslash	=> chr(248),  # small o, slash
 otilde	=> chr(245),  # small o, tilde
 ouml	=> chr(246),  # small o, dieresis or umlaut mark
 szlig	=> chr(223),  # small sharp s, German (sz ligature)
 thorn	=> chr(254),  # small thorn, Icelandic
 uacute	=> chr(250),  # small u, acute accent
 ucirc	=> chr(251),  # small u, circumflex accent
 ugrave	=> chr(249),  # small u, grave accent
 uuml	=> chr(252),  # small u, dieresis or umlaut mark
 yacute	=> chr(253),  # small y, acute accent
 yuml	=> chr(255),  # small y, dieresis or umlaut mark

 # Some extra Latin 1 chars that are listed in the HTML3.2 draft (21-May-96)
 copy   => chr(169),  # copyright sign
 reg    => chr(174),  # registered sign
 nbsp   => chr(160),  # non breaking space

 # Additional ISO-8859/1 entities listed in rfc1866 (section 14)
 iexcl  => chr(161),
 cent   => chr(162),
 pound  => chr(163),
 curren => chr(164),
 yen    => chr(165),
 brvbar => chr(166),
 sect   => chr(167),
 uml    => chr(168),
 ordf   => chr(170),
 laquo  => chr(171),
'not'   => chr(172),    # not is a keyword in perl
 shy    => chr(173),
 macr   => chr(175),
 deg    => chr(176),
 plusmn => chr(177),
 sup1   => chr(185),
 sup2   => chr(178),
 sup3   => chr(179),
 acute  => chr(180),
 micro  => chr(181),
 para   => chr(182),
 middot => chr(183),
 cedil  => chr(184),
 ordm   => chr(186),
 raquo  => chr(187),
 frac14 => chr(188),
 frac12 => chr(189),
 frac34 => chr(190),
 iquest => chr(191),
'times' => chr(215),    # times is a keyword in perl
 divide => chr(247),

 ( $] > 5.007 ? (
   OElig    => chr(338),
   oelig    => chr(339),
   Scaron   => chr(352),
   scaron   => chr(353),
   Yuml     => chr(376),
   fnof     => chr(402),
   circ     => chr(710),
   tilde    => chr(732),
   Alpha    => chr(913),
   Beta     => chr(914),
   Gamma    => chr(915),
   Delta    => chr(916),
   Epsilon  => chr(917),
   Zeta     => chr(918),
   Eta      => chr(919),
   Theta    => chr(920),
   Iota     => chr(921),
   Kappa    => chr(922),
   Lambda   => chr(923),
   Mu       => chr(924),
   Nu       => chr(925),
   Xi       => chr(926),
   Omicron  => chr(927),
   Pi       => chr(928),
   Rho      => chr(929),
   Sigma    => chr(931),
   Tau      => chr(932),
   Upsilon  => chr(933),
   Phi      => chr(934),
   Chi      => chr(935),
   Psi      => chr(936),
   Omega    => chr(937),
   alpha    => chr(945),
   beta     => chr(946),
   gamma    => chr(947),
   delta    => chr(948),
   epsilon  => chr(949),
   zeta     => chr(950),
   eta      => chr(951),
   theta    => chr(952),
   iota     => chr(953),
   kappa    => chr(954),
   lambda   => chr(955),
   mu       => chr(956),
   nu       => chr(957),
   xi       => chr(958),
   omicron  => chr(959),
   pi       => chr(960),
   rho      => chr(961),
   sigmaf   => chr(962),
   sigma    => chr(963),
   tau      => chr(964),
   upsilon  => chr(965),
   phi      => chr(966),
   chi      => chr(967),
   psi      => chr(968),
   omega    => chr(969),
   thetasym => chr(977),
   upsih    => chr(978),
   piv      => chr(982),
   ensp     => chr(8194),
   emsp     => chr(8195),
   thinsp   => chr(8201),
   zwnj     => chr(8204),
   zwj      => chr(8205),
   lrm      => chr(8206),
   rlm      => chr(8207),
   ndash    => chr(8211),
   mdash    => chr(8212),
   lsquo    => chr(8216),
   rsquo    => chr(8217),
   sbquo    => chr(8218),
   ldquo    => chr(8220),
   rdquo    => chr(8221),
   bdquo    => chr(8222),
   dagger   => chr(8224),
   Dagger   => chr(8225),
   bull     => chr(8226),
   hellip   => chr(8230),
   permil   => chr(8240),
   prime    => chr(8242),
   Prime    => chr(8243),
   lsaquo   => chr(8249),
   rsaquo   => chr(8250),
   oline    => chr(8254),
   frasl    => chr(8260),
   euro     => chr(8364),
   image    => chr(8465),
   weierp   => chr(8472),
   real     => chr(8476),
   trade    => chr(8482),
   alefsym  => chr(8501),
   larr     => chr(8592),
   uarr     => chr(8593),
   rarr     => chr(8594),
   darr     => chr(8595),
   harr     => chr(8596),
   crarr    => chr(8629),
   lArr     => chr(8656),
   uArr     => chr(8657),
   rArr     => chr(8658),
   dArr     => chr(8659),
   hArr     => chr(8660),
   forall   => chr(8704),
   part     => chr(8706),
   exist    => chr(8707),
   empty    => chr(8709),
   nabla    => chr(8711),
   isin     => chr(8712),
   notin    => chr(8713),
   ni       => chr(8715),
   prod     => chr(8719),
   sum      => chr(8721),
   minus    => chr(8722),
   lowast   => chr(8727),
   radic    => chr(8730),
   prop     => chr(8733),
   infin    => chr(8734),
   ang      => chr(8736),
  'and'     => chr(8743),
  'or'      => chr(8744),
   cap      => chr(8745),
   cup      => chr(8746),
  'int'     => chr(8747),
   there4   => chr(8756),
   sim      => chr(8764),
   cong     => chr(8773),
   asymp    => chr(8776),
  'ne'      => chr(8800),
   equiv    => chr(8801),
  'le'      => chr(8804),
  'ge'      => chr(8805),
  'sub'     => chr(8834),
   sup      => chr(8835),
   nsub     => chr(8836),
   sube     => chr(8838),
   supe     => chr(8839),
   oplus    => chr(8853),
   otimes   => chr(8855),
   perp     => chr(8869),
   sdot     => chr(8901),
   lceil    => chr(8968),
   rceil    => chr(8969),
   lfloor   => chr(8970),
   rfloor   => chr(8971),
   lang     => chr(9001),
   rang     => chr(9002),
   loz      => chr(9674),
   spades   => chr(9824),
   clubs    => chr(9827),
   hearts   => chr(9829),
   diams    => chr(9830),
 ) : ())
);

=item C<new(app => PARENT)>

Simple constructor.

=item C<init()>

Method called by the constructor.

=cut

my ($k_tab, $k_return, $k_kp_enter, $k_backspace, $k_escape, $k_multiply, $k_home) =
	@Gtk2::Gdk::Keysyms{qw/Tab Return KP_Enter BackSpace Escape KP_Multiply Home/};
#my @k_parse_word = ($k_tab, map ord($_), ' ', qw/. ; , ' "/);

our %UNDO_STEPS = (
	delete     => 'insert',
	insert     => 'delete',
	apply_tag  => 'remove_tag',
	remove_tag => 'apply_tag'
);

$Gtk2::Ex::HyperTextBuffer::TAGS{link} = [foreground => 'blue'];
$Gtk2::Ex::HyperTextBuffer::TAGS{underline} = [background => 'yellow'];

sub new {
	my $class = shift;
	my $self = bless {@_}, $class;
	$self->init();
	return $self;
}

sub init { # called by new()
	my $self = shift;

	my $vbox = Gtk2::VBox->new(0, 0);
	$self->{vbox} = $vbox;
	
	my $scrolled_window = Gtk2::ScrolledWindow->new();
	$scrolled_window->set_policy('automatic', 'automatic');
	$scrolled_window->set_shadow_type('in');
	$vbox->add($scrolled_window);
	$self->{scrolled_window} = $scrolled_window;

	# init TextView
	my $htext = Gtk2::Ex::HyperTextView->new();
	$htext->set_left_margin(10);
	$htext->set_right_margin(5);
	$htext->set_editable(0) if $self->{app}{settings}{read_only};
	$htext->set_tabs( Gtk2::Pango::TabArray->new_with_positions(
		# initial_size, pos_in_pixels, ... allign => position
		1, 0, 'left' => 40 * PANGO_SCALE ) );
	$htext->{link_properties} = [foreground => 'blue']; # TextTag properties
	$htext->signal_connect(link_clicked =>
		sub { $self->{app}->link_clicked($_[1]) }  );
	$htext->signal_connect(link_enter =>
		sub { $self->{app}->push_status("Go to \"$_[1]\"", 'link') }  );
	$htext->signal_connect(link_leave =>
		sub { $self->{app}->pop_status('link') }  );
	$htext->signal_connect_after(toggle_overwrite => \&on_toggle_overwrite, $self);
	$htext->signal_connect(populate_popup => \&on_populate_popup, $self);
	$htext->signal_connect(key_press_event => \&on_key_press_event, $self);
	my $reset_em = sub {
		$self->{buffer}->reset_edit_mode if $self->{buffer};
		return 0;
	};
	$htext->signal_connect(move_cursor => $reset_em);
	$htext->signal_connect(button_press_event => $reset_em);
	$scrolled_window->add($htext);
	$self->{htext} = $htext;

	# init search box
	my $hbox = Gtk2::HBox->new(0, 5);
	$hbox->set_no_show_all(1);
	$hbox->signal_connect(key_press_event => \&on_key_press_event_hbox, $self);
	$vbox->pack_start($hbox, 0, 1, 3);
	$self->{hbox} = $hbox;
	
	my $close_button = Gtk2::Button->new;
	$close_button->set_relief('none');
	$close_button->add(Gtk2::Alignment->new(0.5, 0.5, 0, 0));
	$close_button->child->add(
		Gtk2::Image->new_from_stock('gtk-close', 'small-toolbar') );
	$close_button->signal_connect(clicked => sub { $self->hide_find } );
	$hbox->pack_end($close_button, 0,1,0);
	
	$hbox->pack_start( Gtk2::Label->new(' Find: '), 0,1,0);
	
	my $entry = Gtk2::Entry->new();
	$entry->signal_connect_swapped(changed  => \&on_changed_entry, $self);
	$entry->signal_connect_swapped(activate => \&on_activate_entry,  $self);
	$hbox->pack_start($entry, 0, 1, 0);
	$self->{entry} = $entry;

	my $prev_button = $self->{app}->new_button('gtk-go-back', '_Previous');
	$prev_button->signal_connect(clicked =>
		sub { $self->find($entry->get_text, -1) } );
	$hbox->pack_start($prev_button, 0, 1, 0);
	
	my $next_button = $self->{app}->new_button('gtk-go-forward', '_Next');
	$next_button->signal_connect(clicked =>
		sub { $self->find($entry->get_text, 1) } );
	$hbox->pack_start($next_button, 0, 1, 0);

	# add toolbar buttons and key bindings
	my $accels = $self->{app}{ui}->get_accel_group;
	my $read_only = $self->{app}{settings}{read_only};
	my $actions = Gtk2::ActionGroup->new("PageView");
	$self->{app}{ui}->insert_action_group($actions, 0);
	$actions->add_actions(\@ui_actions_ro, $self);
	unless ($read_only) {
		$actions->add_actions(\@ui_actions, $self);
		$self->{app}{ui}->add_ui_from_string($ui_layout);
		$accels->connect( # ^Y (identical with the shift-^Z defined above)
			ord('Y'), ['control-mask'], ['visible'], sub {$self->redo} );
	}
	$self->{app}{ui}->add_ui_from_string($ui_layout_ro);
	$accels->connect( # alt-/ (identical with the ^F defined above)
		ord('/'), ['mod1-mask'], ['visible'], sub {$self->show_find} );
	$accels->connect( Gtk2::Accelerator->parse('F3'), ['visible'],
		sub { $self->on_FindNext } );

	# prevent our accelerators from triggering while editing another widget
	$htext->signal_connect(focus_in_event  => sub { $actions->set_sensitive(1) });
	$htext->signal_connect(focus_out_event => sub { $actions->set_sensitive(0) });
	
	$htext->set_cursor_visible( $read_only ? 0 : 1 );
}

sub AUTOLOAD {
	my $self = shift;
	$AUTOLOAD =~ s/^.*:://;
	return if $AUTOLOAD eq 'DESTROY';
	return $self->{htext}->$AUTOLOAD(@_);
}

sub on_tag {
	my ($action, $self) = @_;
	my $tag = lc $action->get_name;
	$self->apply_tag($tag);
}

sub on_Link { pop->apply_link }

sub on_SplitPage {
	my $self = pop;
	# TODO prompt level
	$self->{app}->save_page;
	$self->{app}{repository}->split_page($self->{app}{page}, 1);
	$self->{app}->TreeView->{_loaded} = 0;
	$self->{app}->TreeView->load_index;
	$self->{app}->reload;
}

sub on_MergePage {
	my $self = pop;
	$self->{app}->save_page;
	$self->{app}{repository}->merge_page($self->{app}{page});
	$self->{app}->TreeView->{_loaded} = 0;
	$self->{app}->TreeView->load_index;
	$self->{app}->reload;
}

sub on_Undo { pop->undo }

sub on_Redo { pop->redo }

sub on_clipboard {
	my ($action, $self) = @_;
	return unless $self->{htext}->get('has-focus');
	my $signal = lc($action->get_name).'_clipboard';
	$self->{htext}->signal_emit($signal);
}

sub on_Delete {
	my $self = pop;
	return unless $self->{htext}->get('has-focus');
	$self->{htext}->signal_emit('delete_from_cursor', 'chars', 1);
}

sub on_EditLink { pop->edit_link_dialog }

sub on_Find { pop->show_find }

sub on_FindNext { pop->find(undef, 1) }

sub on_FindPrev { pop->find(undef, -1) }

sub on_InsertDate { # insert date that is linked
	my $self = pop;
	my $settings = $self->{app}{settings};
	my $buffer = $self->get_buffer;
	my $string = strftime( $settings->{date_string}, localtime);
	my $link = $settings->{cal_namespace} . strftime('%Y_%m_%d', localtime);
	$buffer->insert_blocks_at_cursor(['link', {to => $link}, $string]);
}

sub on_InsertImage { pop->insert_image }

=item C<widget()>

Returns the root widget. This should be used to add the object to a container widget.
Also use this widget for things like show_all() and hide_all().

=cut

sub widget { return $_[0]->{vbox} }

sub on_key_press_event_hbox {
	my ($hbox, $event, $self) = @_;
	return 0 unless $event->keyval == $k_escape;
	$self->hide_find;
	return 1;
}

sub on_changed_entry {
	my ($self, $entry) = @_;
	$self->find($entry->get_text, 0);
}

sub on_activate_entry {
	my ($self, $entry) = @_;
	$self->{hbox}->hide;
	$self->find($entry->get_text);
	$self->{htext}->grab_focus;
}

=item C<find(STRING, DIRECTION)>

Finds next occurance of STRING in the buffer, scrolls the buffer
and highlights the string. DIRECTION can be either 1 for forward or -1
for backward. If no direction is given a forward search is done including
the current position.

=cut

sub find {
	my ($self, $string, $direction) = @_;
	my $buffer = $self->{buffer};
	my $iter = $buffer->get_iter_at_mark( $buffer->get_insert );
	$iter->forward_char if $direction == 1;
	
	unless (defined $string) {
		# TODO also use selection here - think ^G while entry is hidden - also set entry when selection
		$string = $self->{entry}->get_text;
	}
	
	my ($start, $end);
	if ($direction == -1) { # backward
		($start, $end) = $iter->backward_search($string, 'visible-only');
		unless (defined $start) { # wrap around
			$iter = $buffer->get_end_iter;
			($start, $end) = $iter->backward_search($string, 'visible-only');
			return unless defined $start;
		}
	}
	else { # forward (direction 1 or 0)
		($start, $end) = $iter->forward_search($string, []);
		unless (defined $start) { # wrap around
			$iter = $buffer->get_start_iter;
			($start, $end) = $iter->forward_search($string, []);
			return unless defined $start;
		}
	}
	
	#print "found $string at offset ".$iter->get_offset."\n";
	$self->{buffer}->select_range($start, $end);
	$self->{htext}->scroll_mark_onscreen( $buffer->get_insert );
}

=item C<show_find()>

Show the search bar.

=cut

sub show_find {
	my $self = shift;
	$self->{hbox}->set_no_show_all(0);
	$self->{hbox}->show_all;
	$self->{entry}->grab_focus;
}

=item C<hide_find()>

Hide the search bar.

=cut

sub hide_find {
	my $self = shift;
	$self->{hbox}->hide_all;
	$self->{hbox}->set_no_show_all(1);
	$self->{htext}->grab_focus;
}

sub on_toggle_overwrite {
	my $self = pop;
	my $over = $_[0]->get_overwrite;
	my $sbar = $self->{app}{status2};
	my $id = $sbar->get_context_id('over');
	if ($over) { $sbar->push($id, '  OVR') }
	else       { $sbar->pop($id)           }
}

sub on_populate_popup {
       	# add items to context menu
	my ($htext, $menu, $self) = @_;
	my $iter = $htext->get_iter_at_pointer;
	return unless $iter;
	
	my ($type, $source, $copy_string, $edit_string, $code);
	my $link = $htext->get_link_at_iter($iter);
	if (defined $link) {
		($type, $source) = $self->{app}{page}->parse_link($link);
		$copy_string = ($type eq 'mail')
			? 'Copy Email Address'
			: 'Copy _Link' ;
		$edit_string = '_Edit link';
		$code = sub {$self->edit_link_dialog($iter)};
	}
	elsif (my $pixbuf = $iter->get_pixbuf) {
		return unless defined $pixbuf->{image_src};
		$type = 'image';
		$link = $pixbuf->{image_src};
		$source = $pixbuf->{image_file};
		$copy_string = 'Copy Image _Location';
		$edit_string = '_Edit source';
		$code = sub {$self->edit_image_dialog($iter)};
	}
	else { return }
	
	my $seperator = Gtk2::MenuItem->new();
	$seperator->show;
	$menu->prepend($seperator);
	
	my $edit_item = Gtk2::MenuItem->new($edit_string);
	$edit_item->show;
	$edit_item->signal_connect_swapped(activate => $code, $self);
	$menu->prepend($edit_item);

	my $copy_item = Gtk2::MenuItem->new($copy_string);
	$copy_item->signal_connect(activate => sub {
		my $clipboard = Gtk2::Clipboard->get(
			Gtk2::Gdk::Atom->new('PRIMARY') );
		$clipboard->set_text($link);
	} );
	$copy_item->show;
	$menu->prepend($copy_item);

	if ($type eq 'image' or $type eq 'file') {
		$seperator = Gtk2::MenuItem->new();
		$seperator->show;
		$menu->prepend($seperator);
		Gtk2::Ex::DesktopEntryMenu->populate_menu($menu, $source);
	}
	
	if ($type ne 'image') {
		$seperator = Gtk2::MenuItem->new();
		$seperator->show;
		$menu->prepend($seperator);
	
		my $open_item = Gtk2::MenuItem->new('_Open link');
		$open_item->signal_connect(activate =>
			sub { $self->{app}->link_clicked($link) } );
		$open_item->show;
		$menu->prepend($open_item);
	}
}

sub on_key_press_event { # some extra keybindings
	# FIXME for more consistent behaviour test for selections
	my ($htext, $event, $self) = @_;
	my $val = $event->keyval;

	if ($self->{app}{settings}{read_only}) {
		return 0 unless $val == ord('/');
		$self->show_find;
		return 1;
	}
	
	if ($val == $k_return or $val == $k_kp_enter) { # Enter
		my $buffer = $htext->get_buffer;
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		return 1 if defined $htext->click_if_link_at_iter($iter);
		$self->parse_word($iter); # end-of-line is also end-of-word
		$iter = $buffer->get_iter_at_mark($buffer->get_insert());
		$self->parse_line($iter) or return 0;
		$htext->scroll_mark_onscreen( $buffer->get_insert );
		return 1;
	}
	elsif (
		$self->{app}{settings}{backsp_unindent} and $val == $k_backspace or
		$val == $k_tab and $event->state eq 'shift-mask'
	) { # BackSpace or Shift-Tab
		my $buffer = $htext->get_buffer;
		my ($start, $end) = $buffer->get_selection_bounds;
		if ($end and $end != $start) {
			my $cont = $self->selection_backspace($start, $end);
			return $val == $k_tab ? 1 : $cont;
		}
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		if ($self->parse_backspace($iter)) {
			$htext->scroll_mark_onscreen( $buffer->get_insert );
			return 1;
		}
	}
	elsif ($val == $k_tab or $val == ord(' ')) { # WhiteSpace
		my $buffer = $htext->get_buffer;
		if ($val == $k_tab) {
			my ($start, $end) = $buffer->get_selection_bounds;
			if ($end and $end != $start) {
				$self->selection_tab($start, $end);
				return 1;
			}
		}
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		my $string = ($val == $k_tab) ? "\t" : ' ';
		if ($self->parse_word($iter, $string)) {
			$htext->scroll_mark_onscreen( $buffer->get_insert );
			return 1;
		}
	}
	elsif ($val == ord('*') or $val == $k_multiply) { # Bullet
		my $buffer = $htext->get_buffer;
		my ($start, $end) = $buffer->get_selection_bounds;
		return 0 if !$end or $end == $start;
		$self->toggle_bullets($start, $end);
		return 1;
	}
	elsif ($val == $k_home and not $event->state >= 'control-mask') { # Home toggle
		my $buffer = $htext->get_buffer;
		my $insert = $buffer->get_iter_at_mark($buffer->get_insert());
		my $start  = $insert->copy;
		$htext->backward_display_line_start($start)
			unless $htext->starts_display_line($start);
		my $begin  = $start->copy;
		my $indent = '';
		while ($indent =~ /^\s*([^\s\w]\s*)?$/) {
			last if $begin->ends_line or ! $begin->forward_char;
			$indent = $start->get_text($begin);
		}
		$indent =~ /^(\s*([^\s\w]\s+)?)/;
		my $back = length($indent) - length($1);
		$begin->backward_chars($back) if $back > 0;
		$insert = ($begin->ends_line || $insert->equal($begin)) ? $start : $begin;
		if ($event->state >= 'shift-mask') {
			$buffer->move_mark_by_name('insert', $insert);
			# leaving the "selection_bound" mark behind
		}
		else { $buffer->place_cursor($insert) }
		return 1;
	}
		
	#else { printf "key %x pressed\n", $val } # perldoc -m Gtk2::Gdk::Keysyms

	return 0;
}

=item C<get_state()>

Returns a number of properties that need to be saved in the history.

=cut

sub get_state {
	my $self = shift;
	my $buffer = $self->{buffer} || return;
	my $cursor = $buffer->get_iter_at_mark($buffer->get_insert)->get_offset;
	#my $vscroll = $self->{scrolled_window}->get_vadjustment->get_value;
	#my $hscroll = $self->{scrolled_window}->get_hadjustment->get_value;
	return cursor => $cursor, undo => $self->{undo}, redo => $self->{redo};
		#, vscroll => $vscroll, hscroll => $hscroll;
}

=item C<set_state(property => value, ..)>

Set a number of properties that could be saved in the history.

=cut

sub set_state {
	my $self = shift;
	my %rec = @_;
	
	if (defined $rec{cursor}) {
		$self->{buffer}->place_cursor(
			$self->{buffer}->get_iter_at_offset($rec{cursor}) );
	
		$self->{htext}->scroll_mark_onscreen( $self->{buffer}->get_insert );
	}
	
	#$self->{scrolled_window}->get_vadjustment->set_value($rec{vscroll});
	#$self->{scrolled_window}->get_hadjustment->set_value($rec{hscroll});
	
	$self->{undo} = $rec{undo} || [];
	$self->{redo} = $rec{redo} || [];
}

=item C<load_page(PAGE)>

Load a new page object into the buffer.

=cut

sub load_page {
	my ($self, $page) = @_;
	my $use_spell = defined $self->{app}{objects}{Spell}; # FIXME ugly internals
	
	# clear the old buffer
	$self->{undo_lock} = 1;
	$self->{buffer}->clear if $self->{buffer};
	$self->{app}->Spell->detach($self->{htext}) if $use_spell;
	$self->{_prev_buffer} = $self->{buffer}; # FIXME hack to prevent segfaults
	
	# create a new HyperTextBuffer
	my $buffer = Gtk2::Ex::HyperTextBuffer->new();
	$buffer->create_default_tags;
#	$buffer->create_tag('info', editable => 0); #, foreground => 'red');
	$self->{buffer} = $buffer;
	$self->{htext}->set_buffer($buffer);
	my $tree = $page->get_parse_tree;
	$buffer->set_parse_tree($tree);
	unless ($self->{app}{settings}{read_only}) {
		# connect signals _after_ load_parsetree()
		$buffer->signal_connect(delete_range => \&on_delete_range, $self);
		$buffer->signal_connect_after(insert_text => \&on_insert_text, $self);
		$buffer->signal_connect(modified_changed =>
			sub {$self->{app}->update_status} );
		$buffer->signal_connect(apply_tag => \&on_apply_tag, $self);
		$buffer->signal_connect(remove_tag => \&on_remove_tag, $self);
	}
	$self->{app}->Spell->attach($self->{htext}) if $use_spell;
	$self->{undo_lock} = 0;
	
	if ($page->exists) {
		$buffer->place_cursor(
	 	      	$buffer->get_iter_at_offset(0) );
	}
	else { # new page, place cursor below headers but above footer
		my (undef, $iter) = $buffer->get_bounds;
#		$iter->backward_to_tag_toggle( $buffer->get_tag_table->lookup('info') );
		$buffer->place_cursor( $iter );
	}
	
	$self->{undo} = [];
	$self->{redo} = [];
	
	$self->{htext}->scroll_mark_onscreen( $buffer->get_insert );

	$buffer->set_modified(0);
}

=item C<modified()>

=item C<modified(BOOLEAN)>

Get or set the modified bit. This bit is set when the buffer
is modified by the user.
It should be reset after succesfully saving the buffer content.

=cut

sub modified {
	return 0 unless defined $_[0]->{buffer};
	$_[0]->{buffer}->set_modified($_[1]) if defined $_[1];
	$_[0]->{buffer}->get_modified;
}

=item C<get_parse_tree(PAGE)>

Returns the parse tree for the current buffer contents.

=cut

sub get_parse_tree {
	my $tree = $_[0]->{buffer}->get_parse_tree;
#	my @head = splice @$tree, 0, 2;
#	@$tree = grep {not ref($_) && $$_[0] eq 'info'} @$tree;
#	unshift @$tree, @head;
	return $tree;
}

=item C<parse_backspace(ITER)>

This method is called when the user types a backspace.
It tries to update the formatting of the current line.
 
When TRUE is returned the widget does not recieve the backspace.

=cut

sub parse_backspace {
	my ($self, $iter) = @_;
	my $buffer = $self->{buffer};
	my $lf = $buffer->get_iter_at_line( $iter->get_line );
	my $line = $buffer->get_text($lf, $iter, 0);
	if ($line =~ s/\t([\*\x{2022}]\s)$/$1/) {
		$buffer->delete($lf, $iter);
		$buffer->insert($lf, $line);
		return 1;
	}
	return 0;
}

=item C<parse_line(ITER)>

This method is called when the user is about to insert a linebreak.
It checks the line left of the cursor of any markup that needs 
updating. It also takes care of autoindenting.

When TRUE is returned the widget does not recieve the linebreak.

=cut

sub parse_line {
	my ($self, $iter) = @_;
	my $buffer = $self->{buffer};
	my $lf = $buffer->get_iter_at_line( $iter->get_line );
	my $line = $buffer->get_text($lf, $iter, 0);
	#print ">>$line<<\n";
	if ($line =~ s/^(=+)\s*(\w)/$2/) { # heading
		my $offset;
		($lf, $offset) = ($lf->get_offset, $iter->get_offset);
		$buffer->insert($iter, "\n"); # keep undo order
		($lf, $iter) = map $buffer->get_iter_at_offset($_), $lf, $offset;
		$iter->forward_char;
		my $h = length($1); # no (7 - x) monkey bussiness here
		$h = 5 if $h > 5;
		$line =~ s/\s+=+\s*$//;
		$offset = $lf->get_offset + length $line;
		$buffer->delete($lf, $iter);
		$buffer->insert_with_tags_by_name($lf, $line, "head$h");
		$iter = $buffer->get_iter_at_offset($offset);
		$buffer->insert($iter, "\n");
		return 1;
	}
	elsif ($line =~ /^(\s*(:?\W+|\d+\W?|\w\W)\s+|\s+)$/) { # empty bullet or list item
		# TODO check previous line for same pattern !
		$buffer->delete($lf, $iter);
	}
	elsif ($line =~ /^(\s*(:?\W+|\d+\W?|\w\W)\s+|\s+)/) { # auto indenting + lists
		my ($indent, $number) = ($1, $2);
		if (length $number) { # numbered list
			$number = ($number =~ /\d/) ? $number+1 : chr(ord($number)+1);
			$indent =~ s/^(\s*)(\d+|\w)/$1$number/;
		}
		my $offset = $iter->get_offset;
		$buffer->insert($iter, "\n");
		$iter = $buffer->get_iter_at_offset($offset);
		$iter->forward_char;
		$buffer->insert($iter, "$indent");
		$self->{htext}->scroll_mark_onscreen( $buffer->get_insert() );
		return 1;
	}
	return 0;
}

=item C<parse_word(ITER, CHAR)>

This method is called after the user ended typing a word.
It checks the word left of the cursor for any markup that
needs updating.

CHAR can be the key that caused a word to end, returning TRUE
makes it never reaching the widget.

=cut

my @_link_reg = (
	qr{(?<!\S)(\w+://\S*|[\w:]*:\w{2,}:?)$},
	qr{(?<!\S)(\w+://\S*|[\w:]*:\w{2,}:?|[A-Z]+[^\WA-Z]+[A-Z]+\w*)$}
);

sub parse_word {
	# remember that $char can be empty
	# first insert the char, then replace it, keep undo stack in proper order
	my ($self, $iter, $char) = @_;
	return 0 if $self->_is_verbatim($iter);
	my $buffer = $self->{buffer};
	my $lf = $iter->copy;
	$self->{htext}->backward_display_line_start($lf)
		unless $self->{htext}->starts_display_line($lf);
	my $line = $buffer->get_text($lf, $iter, 0);
	#warn ">>$line<< >>$char<<\n";
	if ($line =~ /^(\s*)[\*\x{2022}](\s*)$/) { # bullet
		return unless $lf->starts_line; # starts_display_line != starts_line
		my ($pre, $post) = ($1, $2.$char);
		my $offset;
		if (defined $char) {
			($lf, $offset) = ($lf->get_offset, $iter->get_offset);
			$buffer->insert($iter, $char); # get undo order good
			($lf, $iter) = map $buffer->get_iter_at_offset($_), $lf, $offset;
			$iter->forward_char;
			$pre .= $1 if $post =~ s/(\t+)$//; # switch tabs
			$post = ' ' unless length $post;
		}
		$line = $pre."\x{2022}".$post;
		$buffer->delete($lf, $iter);
		$buffer->insert($lf, $line);
		return 1;
	}
	elsif ($line =~ $_link_reg[$self->{app}{settings}{use_camelcase}]) { # CamelCase or external link
		my $word = $1;
		my ($start, $end) = ($iter->copy, $iter->copy);
		$start->backward_chars(length $word);
		return 0 if $start->get_tags or $end->get_tags;
		if (defined $char) {
			($start, $end) = ($start->get_offset, $end->get_offset);
			$buffer->insert($iter, $char);
			($start, $end) = map $buffer->get_iter_at_offset($_), $start, $end;
		}
		$self->{htext}->apply_link(undef, $start, $end);
		return 1;
		
	}
	elsif ( $self->{app}{settings}{use_utf8_ent} &&
		$line =~ /(?<!\S)\\(\w+)$/) { # utf8 chars
		my $word = $1;
		my $chr = ($word =~ /^\d+$/) ? chr($word) :
			exists($entity2char{$word}) ? $entity2char{$word} : undef ;
		return 0 unless defined $chr;
		if (defined $char) {
			my $offset = $iter->get_offset;
			$buffer->insert($iter, $char);
			$iter = $buffer->get_iter_at_offset($offset)
		}
		my $begin = $iter->copy;
		$begin->backward_chars(1 + length $word);
		$buffer->delete($begin, $iter);
		$buffer->insert($begin, $chr);
		return 1;
	}
#	elsif ($line =~ /^(\t|  )/) { # pre
#		# FIXME \s* => \t
#		$iter->forward_char unless $iter->is_end; # FIXME somthing at end
#		$buffer->apply_tag_by_name('pre', $lf, $iter);
#	}
	
	return 0;
}

sub _is_verbatim {
	my ($self, $iter) = @_;
	for ($iter->get_tags) {
		return 1 if lc($_->get_property('name')) eq 'verbatim';
	}
	return 0;
}

=item C<insert_image(IMAGE)>

Inserts an image into the buffer. Without argument prompts the user for a file.

=cut

sub insert_image {
	my ($self, $file) = @_;
	$file = $self->{app}->filechooser_dialog unless $file;
	return unless length $file;
	# TODO check relativeness of filename etc.
	$self->{buffer}->insert_image_from_file($file);
}

=item C<apply_tag(TAG)>

Applies the tag with the name TAG to any selected text.

=cut

sub apply_tag {
	my ($self, $tag) = @_;
	my $buffer = $self->{buffer};
	my ($start, $end) = $buffer->auto_selection($tag);
	return unless defined $start;

	if ($tag eq 'verbatim') {
		my $Verbatim = $buffer->get_tag_table->lookup('Verbatim');
		$tag = 'Verbatim'
			if $start->get_line != $end->get_line
			or $start->starts_line && $end->ends_line
			or $start->has_tag($Verbatim)
			or $end->has_tag($Verbatim)  ;
	}
	
	$buffer->remove_all_tags($start, $end);
	$buffer->apply_tag_by_name($tag, $start, $end)
		unless $tag eq 'normal';
	$buffer->set_modified(1);

	if ($tag =~ /^head/) { # give headers their own line
		$end = $end->ends_line ? undef : $end->get_offset ;
		$buffer->insert($start, "\n") unless $start->starts_line;
		$buffer->insert($buffer->get_iter_at_offset($end+1), "\n")
			unless ! defined $end;
	}
}

=item C<apply_link(LINK)>

This method is called by the "Link" button or by the ^L keybinding.
It makes any selected text a link. This link is followed immediatly
if the 'follow_new_link' config option is set.

If LINK is undefined the link target is the same as the selected text.

If no text is selected it calls the "New Link" dialog.

In readonly modus the selected text is regarded as a link and
followed immediatly, but no actual link is made

=cut

sub apply_link {
	my ($self, $link) = @_;
	my $buffer = $self->{buffer};
	my ($start, $end) = $buffer->auto_selection('link');
	
	unless (defined $start) {
		return $self->{app}{settings}{read_only}
			? $self->{app}->goto_page_dialog
			: $self->edit_link_dialog ;
	}

	my $text = $buffer->get_text($start, $end, 0);
	$link = $text unless defined $link;
	return undef if $link =~ /\n/;
	
	unless ($self->{app}{settings}{read_only}) {
		my $bit = $link eq $text;
		$buffer->remove_all_tags($start, $end);
		$self->{htext}->apply_link(($bit ? undef : $link), $start, $end);
		$buffer->set_modified(1);
	}

	$self->{app}->link_clicked($link)
		if $self->{app}{settings}{read_only}
		or $self->{app}{settings}{follow_new_link};
}

=item C<toggle_bullets()>

If selected text is a bullet list this removes the bullets, else it adds
bullets.

=cut

sub toggle_bullets {
	my ($self, $start, $end) = @_;
	my $buffer = $self->{buffer};
	($start, $end) = $buffer->get_selection_bounds unless defined $start;
	return if !$end or $start == $end;
	
	my $text = $self->{buffer}->get_text($start, $end, 1);
	if ($text =~ /^\s*[\*\x{2022}]\s+/m) { # remove bullets
		$text =~ s/^(\s*)[\*\x{2022}]\s+/$1/mg
	}
	else { # set bullets
		$text =~ s/^(\s*)(\S)/$1\x{2022} $2/mg;
	}

	$buffer->replace_selection($text);
}

=item C<selection_tab()>

Puts a tab before every line of a selection.

=cut

sub selection_tab {
	my ($self, $start, $end) = @_;
	($start, $end) = $self->{buffer}->get_selection_bounds unless defined $start;
	return if !$end or $start == $end;
	
	my $buffer = $self->{buffer};
	my $text = $buffer->get_text($start, $end, 1);
	$text =~ s/^/\t/mg;

	my $verbatim = $self->_is_verbatim($start);
	($start, $end) = $buffer->replace_selection($text);
	$self->{buffer}->apply_tag_by_name('verbatim', $start, $end)
		if $verbatim;
}

=item C<selection_backspace()>

Removes a tab for every line of a selection.

=cut

sub selection_backspace {
	my ($self, $start, $end) = @_;
	my $buffer = $self->{buffer};
	($start, $end) = $buffer->get_selection_bounds unless defined $start;
	return if !$end or $start == $end;

	my $text = $self->{buffer}->get_text($start, $end, 1);
	my $verbatim = $self->_is_verbatim($start);
	if ($text =~ s/^\t//mg) {
		($start, $end) = $buffer->replace_selection($text);
		$self->{buffer}->apply_tag_by_name('Verbatim', $start, $end)
			if $verbatim;
		return 1;
	}

	return 0;
}

sub on_insert_text { # buffer, iter, string, length, self
#	(length($string) == 1)
#		? push(@undo_chars, $string)
#		: 
	$_[4]->add_undo('insert', $_[1]->get_offset - length($_[2]), $_[2]);
}

sub on_delete_range { # buffer, begin, end, self
	#print "delete range\n";
	my $string = $_[0]->get_text($_[1], $_[2], 0);
	$_[3]->add_undo('delete', $_[1]->get_offset, $string);
}

sub on_apply_tag { pop->_on_change_tag('apply_tag', @_) }

sub on_remove_tag { pop->_on_change_tag('remove_tag', @_) }

sub _on_change_tag {
	my ($self, $action, undef, $tag, $start, $end) = @_;
	my @off = ($start->get_offset, $end->get_offset);
	if ($tag->{is_link}) {
		$self->add_undo($action, @off, 'L', $tag->{link_data});
	}
	else {
		$self->add_undo($action, @off, $tag->get_property('name'));
	}
}

=item C<add_undo(ACTION, OFFSET, DATA)>

=cut

sub add_undo {
	my $self = shift;
	return if $self->{undo_lock}; # prohibit unwanted recursion
#	flush_undo_chars() if @undo_chars;
	my ($action, $offset, @data) = @_;
#	print "do: $action \@$offset: >>@data<<\n";
	push @{$self->{undo}}, [$action, $offset, @data];
	shift @{$self->{undo}} if @{$self->{undo}} > $self->{app}{settings}{undo_max};
	@{$self->{redo}} = ();
}

#sub flush_undo_chars {
#	return unless @undo_chars;
#	add_undo('insert', 
#}

=item C<undo()>

Undo one editing step in the buffer.

=cut

sub undo {
	my $self = shift;
	my ($undo, $redo) = @{$self}{'undo', 'redo'};
	return unless @$undo;
	my $step = pop @$undo;
	unshift @$redo, [@$step]; # force copy;
	$$step[0] = $UNDO_STEPS{$$step[0]};
	$self->_do_step(@$step);
}

=item C<redo()>

Redo one editing step in the buffer.

=cut

sub redo {
	my $self = shift;
	my ($undo, $redo) = @{$self}{'undo', 'redo'};
	return unless @$redo;
	my $step = shift @$redo;
	push @$undo, $step;
	$self->_do_step(@$step);
}

sub _do_step {
	my ($self, $action, $offset, @data) = @_;
	my $buffer = $self->{buffer};
	my $start = $buffer->get_iter_at_offset($offset);
	$self->{undo_lock} = 1;
	if ($action eq 'insert') {
		$buffer->insert($start, $data[0]);
		$buffer->place_cursor(
			$buffer->get_iter_at_offset($offset + length($data[0])));
	}
	elsif ($action eq 'delete') {
		my $end = $buffer->get_iter_at_offset($offset + length($data[0]));
		$buffer->delete($start, $end);
		$buffer->place_cursor($start);
	}
	elsif ($action eq 'apply_tag') {
		my $end = $buffer->get_iter_at_offset( $data[0] );
		$buffer->remove_all_tags($start, $end);
		if ($data[1] eq 'link') {
			$self->{htext}->apply_link($data[2], $start, $end);
		}
		else { $buffer->apply_tag_by_name($data[1], $start, $end) }
	}
	elsif ($action eq 'remove_tag') {
		my $end = $buffer->get_iter_at_offset( $data[0] );
		$buffer->remove_all_tags($start, $end);
	}
	$buffer->set_modified(1);
	$self->{htext}->scroll_mark_onscreen( $buffer->get_insert );
	$self->{undo_lock} = 0;
}

#sub flush_undo_mini {
#	return unless @undo_mini;
#	my (prev,
#	for (@undo_mini) {
#		my ($action, $offset, $char) = @$_;
#		
#}

=item C<edit_link_dialog(ITER)>

This dialog allows the user to create a link for which
the link target and the link text differ.

If ITER is undefined the cursor is used.

=cut

sub edit_link_dialog {
	my ($self, $iter) = @_;
	my $buffer = $self->{buffer};

	# handle selections
	my ($start, $end) = $buffer->auto_selection('link', $iter);

	my ($text, $link) = ('', '');
	if (defined $start) {
		$link = $self->{htext}->get_link_at_iter($start);
		$text = $self->{buffer}->get_text($start, $end, 0);
		$text = undef if $text =~ /\n/;
	}

	# Run the dialog
	my $title = defined($start) ? 'Edit Link' : 'Insert Link';
	my $val = $self->{app}->run_prompt( $title,
		['Text', 'Links to'], [$text, $link], $LINK_ICON, '_Link' )
		or return;
	($text, $link) = @$val;
	return unless $text =~ /\S/ or $link =~ /\S/;
	
	# both entries default to the other
	$link = $text unless $link =~ /\S/;
	$text = $link unless $text =~ /\S/;

	# use delete + insert instead of apply because the text can be different
	if (defined $start) {
		$buffer->delete($start, $end);
	}
	else {
		$start = $buffer->get_iter_at_mark( $buffer->get_insert());
	}
	my $bit = $link eq $text;
	my $_start = $start->get_offset;
	$buffer->insert($start, $text);
	$start = $buffer->get_iter_at_offset($_start);
	$end = $start->copy;
	$end->forward_chars(length $text);
	$buffer->remove_all_tags($start, $end); # because of this we can't use htext->insert_link()
	$self->{htext}->apply_link(($bit ? undef : $link), $start, $end);
	# FIXME redundant code from "apply_link" here
}

=item C<edit_image_dialog()>

=cut

sub edit_image_dialog { # TODO add 'browse' button
	my ($self, $iter) = @_;
	my $pixbuf = $iter->get_pixbuf;
	return unless $pixbuf and defined $pixbuf->{image_src};
	
	my $src = $pixbuf->{image_src};
	my $val = $self->{app}->run_prompt( 'Edit Image Source',
		['Source'], [$src], $LINK_ICON, '_Link' )
		or return;

	my ($new_src) = @$val;
	return unless $new_src =~ /\S/ and $new_src ne $src;

	my $buffer = $self->{buffer};
	my $end = $iter->copy;
	$end->forward_char;
	$buffer->delete($iter, $end);
	$buffer->place_cursor($iter);
	$self->insert_image($new_src);
}

1;

__END__

=back

=head1 AUTHOR

Jaap Karssenberg (Pardus) E<lt>pardus@cpan.orgE<gt>

Copyright (c) 2005 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

=cut

